package gpstools.data;

import generated.tcx.AbstractSourceT;
import generated.tcx.ActivityLapT;
import generated.tcx.ActivityT;
import generated.tcx.ApplicationT;
import generated.tcx.BuildT;
import generated.tcx.DeviceT;
import generated.tcx.HeartRateInBeatsPerMinuteT;
import generated.tcx.SportT;
import generated.tcx.TrackT;
import generated.tcx.TrackpointT;
import generated.tcx.TrainingCenterDatabaseT;
import generated.tcx.VersionT;
import generated.tcxext2.ActivityLapExtensionT;
import generated.tcxext2.ExtensionsT;
import gpstools.track.LapInfo;
import gpstools.track.Track;
import gpstools.track.TrackInfo;
import gpstools.track.TrackInfo.ActivityType;
import gpstools.track.addons.Source;
import gpstools.track.addons.SourceType;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;


/**
 * TCX Loader
 */
public class TcxLoader implements Loader, TrackSource {
    private static final String EXTENSION = "tcx";
	private static EnumMap<SportT, ActivityType> activityTypeMapper;

	{
		activityTypeMapper = new EnumMap<SportT, TrackInfo.ActivityType>(SportT.class);
		activityTypeMapper.put(SportT.BIKING, ActivityType.CYCLING);
		activityTypeMapper.put(SportT.RUNNING, ActivityType.RUNNING);
		activityTypeMapper.put(SportT.OTHER, ActivityType.OTHER);
	}

	private Queue<Track> tracks = new LinkedList<Track>();
	private Source source;
	
    TcxLoader() {
    }

    @Override
    public String getExtension() {
        return EXTENSION;
    }

    public TrackSource load(String fileName) throws IOException, UnsupportedFormatException {
        return new TcxLoader(fileName);
    }
    
    @Override
    public TrackSource load(byte[] data) throws UnsupportedFormatException {
        return new TcxLoader(data);
    }

    public TcxLoader(String fileName) throws FileNotFoundException, UnsupportedFormatException {
        this(new FileInputStream(fileName));
    }

    public TcxLoader(byte[] data) throws UnsupportedFormatException {
        this(new ByteArrayInputStream(data));
    }

    public TcxLoader(InputStream inputStream) throws UnsupportedFormatException {
        final TrainingCenterDatabaseT database = unmarshal(inputStream).getValue();

        if (database == null || database.getActivities() == null) {
            throw new UnsupportedFormatException("no data in the tcx file");
        }


        List<ActivityT> activities = database.getActivities().getActivity();
        for (ActivityT activity : activities) {
            Track.Builder builder = Track.builder();
            processActivity(activity, builder);
        }
        
        source = processCreator(database.getAuthor());
    }

	private void processActivity(ActivityT activity, Track.Builder builder) {
	    builder.name(activity.getId().toXMLFormat());
		for (ActivityLapT lap : activity.getLap()) {
		    processLap(builder, lap);
		}
		if (!builder.isEmpty()) {
		    Track track = builder.build();
			tracks.add(track);
			TrackInfo info = track.getInfo();
			
			ActivityType activityType = activityTypeMapper.get(activity.getSport());
			info.setActivityType(activityType);
			
			info.setSource(processCreator(activity.getCreator()));
		}
	}

	private static Source processCreator(AbstractSourceT creator) {
		Map<String, String> properties = new HashMap<String, String>();
		if (creator instanceof DeviceT) {
			DeviceT device = (DeviceT) creator;
			properties.put("productId", Integer.toString(device.getProductID()));
			properties.put("unitId", Long.toString(device.getUnitId()));
			processVersion(properties, device.getVersion());
			return new Source(creator.getName(), SourceType.DEVICE, properties);
		} else if (creator instanceof ApplicationT) {
			ApplicationT application = (ApplicationT) creator;
			properties.put("partNumber", application.getPartNumber());
			properties.put("langId", application.getLangID());
			BuildT build = application.getBuild();
			if (build != null) {
				processVersion(properties, build.getVersion());
			}
			return new Source(creator.getName(), SourceType.APPLICATION, properties);
		} else {
			throw new IllegalArgumentException("unsupported creator type" + creator);
		}
	}

	private static void processVersion(Map<String, String> properties, VersionT version) {
		if (version != null) {
			properties.put("version.versionMajor", Integer.toString(version.getVersionMajor()));
			properties.put("version.versionMinor", Integer.toString(version.getVersionMinor()));
			properties.put("version.buildMajor", Integer.toString(version.getBuildMajor()));
			properties.put("version.buildMinor", Integer.toString(version.getBuildMinor()));
		}
	}

	private void processLap(Track.Builder builder, ActivityLapT lap) {
		for (TrackT track : lap.getTrack()) {
		    for (TrackpointT point : track.getTrackpoint()) {
		        builder.add(TcxConverter.pointFromTcx(point));
		    }
		}
	    LapInfo info = builder.closeLap();
	    info.setTotalTime(lap.getTotalTimeSeconds());
		info.setAverageHeartRate(extractHeartRate(lap.getAverageHeartRateBpm()));
		info.setMaximumHeartRate(extractHeartRate(lap.getMaximumHeartRateBpm()));
		info.setDistance(lap.getDistanceMeters());
		info.setCalories(lap.getCalories());
		info.setMaximumSpeed(lap.getMaximumSpeed());
		info.set("garmin.tcx.lapIntensity", lap.getIntensity().value());
		info.set("garmin.tcx.lapTriggerMethod", lap.getTriggerMethod().value());
		if (lap.getExtensions() == null){
			return;
		}
		List<Object> extensions = lap.getExtensions().getAny();
		for (Object extension : extensions) {
			// TODO refactoring
			if (extension instanceof JAXBElement<?>) {
				Object value = ((JAXBElement<?>) extension).getValue();
				if (value instanceof ActivityLapExtensionT) {
					ActivityLapExtensionT lx = (ActivityLapExtensionT) value;
					info.setAverageSpeed(lx.getAvgSpeed());
				}
			}
		}
	}

    private static Short extractHeartRate(HeartRateInBeatsPerMinuteT tcxHeartRate) {
    	if (tcxHeartRate == null){
    		return null;
    	}
		return tcxHeartRate.getValue();
	}

	private static JAXBElement<TrainingCenterDatabaseT> unmarshal(InputStream input) throws UnsupportedFormatException {
        try {
            JAXBContext context = JAXBContext.newInstance(TrainingCenterDatabaseT.class, ExtensionsT.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            @SuppressWarnings("unchecked")
            JAXBElement<TrainingCenterDatabaseT> root = (JAXBElement<TrainingCenterDatabaseT>) unmarshaller
                    .unmarshal(input);
            return root;
        } catch (JAXBException e) {
            throw new UnsupportedFormatException(e);
        }
    }

    @Override
    public Track next() {
        return tracks.poll();
    }

	public List<Track> all() {
		ArrayList<Track> list = new ArrayList<Track>();
		Track track;
		while ((track = next()) != null) {
			list.add(track);
		}
		return list;
	}
	
	public Source getSource() {
		return source;
	}

}
